// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Linq;

namespace SnapObjects.Expressions
{
    internal abstract class MetadataManager<T> : IMetadataManager<T>
    {
        protected static readonly object _lock = new object();
        protected static readonly IDictionary<string, MetadataCollection<T>> _metadatas;

        protected readonly ILogger _logger;
        protected readonly IServiceProvider _services;
        protected readonly IDictionary<string, List<IMetadataProvider<T>>> _providers;

        static MetadataManager()
        {
            _metadatas = new Dictionary<string, MetadataCollection<T>>(
                StringComparer.OrdinalIgnoreCase);
        }

        public MetadataManager(IServiceProvider services, ILogger logger)
        {
            _logger = logger;
            _services = services;

            _providers = new Dictionary<string, List<IMetadataProvider<T>>>();
            foreach (var metadataProvider in services.GetServices<IMetadataProvider<T>>())
            {
                foreach (var kind in metadataProvider.SupportKinds)
                {
                    if (!_providers.TryGetValue(kind, out var list))
                    {
                        list = new List<IMetadataProvider<T>>();

                        _providers[kind] = list;
                    }

                    list.Add(metadataProvider);
                }
            }
        }

        public virtual bool TryGetMetadata(
            MetadataType type, object name, ParserOptions<T> options, out Metadata<T> metadata)
        {
            var hasMetadata = false;
            Metadata<T> multiMetadata = null;

            if (_providers.TryGetValue(options.SourceKind, out var providers))
            {
                foreach (var provider in providers)
                {
                    if (provider.Type == type &&
                        provider.TryGet(name, options, out metadata))
                    {
                        if (this.CanMergeMultiResult(type))
                        {
                            hasMetadata = true;

                            multiMetadata = this.MergeMethodMetadata(multiMetadata, metadata);
                        }
                        else
                        {
                            return true;
                        }
                    }
                }
            }

            if (_metadatas.TryGetValue(options.SourceKind, out var collection))
            {
                if (collection.TryGetMetadata(type, name, options, out metadata))
                {
                    if (this.CanMergeMultiResult(type))
                    {
                        hasMetadata = true;

                        multiMetadata = this.MergeMethodMetadata(multiMetadata, metadata);
                    }
                    else
                    {
                        return true;
                    }
                }
            }

            if (options != null &&
                options.Metadatas.TryGetMetadata(
                MetadataType.Parameter, name, null, out var meta))
            {
                if (this.CanMergeMultiResult(type))
                {
                    hasMetadata = true;

                    multiMetadata = this.MergeMethodMetadata(multiMetadata, meta);
                }
                else
                {
                    metadata = meta;

                    return true;
                }
            }

            if (hasMetadata)
            {
                metadata = multiMetadata;

                return true;
            }

            _logger.LogInformation(
                $"No metadata provider for kind {options.SourceKind} was found!");

            metadata = null;

            return false;
        }

        public virtual bool AddMetadataProvider(IMetadataProvider<T> provider)
        {
            foreach (var kind in provider.SupportKinds)
            {
                this.GetCollection(kind).AddMetadataProvider(provider);
            }

            return true;
        }

        public virtual bool AddMetadata(Metadata<T> metadata, string sourceKind)
        {
            return this.GetCollection(sourceKind).AddMetadata(metadata);
        }

        public virtual bool AddGroup(string name, IEnumerable<string> conditions, string sourceKind)
        {
            return this.GetCollection(sourceKind).AddGroup(name, conditions);
        }

        public virtual bool AddGroup(string name, Func<object, object> keySelector, string sourceKind)
        {
            return this.GetCollection(sourceKind).AddGroup(name, keySelector);
        }

        public virtual bool AddMethod<TClass>(string sourceKind) where TClass : class
        {
            return this.GetCollection(sourceKind).AddMethod<TClass>();
        }

        public virtual bool AddMethod<TClass>(TClass instance, string sourceKind) where TClass : class
        {
            return this.GetCollection(sourceKind).AddMethod(instance);
        }

        public virtual bool AddMethod(string key, Delegate method, string sourceKind)
        {
            return this.GetCollection(sourceKind).AddMethod(key, method);
        }

        public virtual bool AddParameter(string name, Type type, string sourceKind)
        {
            return this.GetCollection(sourceKind).AddParameter(name, type);
        }

        public virtual bool AddVariable(string name, object value, string sourceKind)
        {
            return this.GetCollection(sourceKind).AddVariable(name, value);
        }

        protected virtual MetadataCollection<T> GetCollection(string sourceKind)
        {
            if (!_metadatas.TryGetValue(sourceKind, out var collection))
            {
                lock (_lock)
                {
                    if (!_metadatas.TryGetValue(sourceKind, out collection))
                    {
                        collection = this.CreateCollection();

                        _metadatas[sourceKind] = collection;
                    }
                }
            }

            return collection;
        }

        protected virtual bool CanMergeMultiResult(MetadataType type)
        {
            return type == MetadataType.Method;
        }

        protected abstract Metadata<T> MergeMethodMetadata(Metadata<T> metadata, Metadata<T> from);

        protected abstract MetadataCollection<T> CreateCollection();

        protected void AddMethods<TData, TParam>(
            MethodMetadata<TData, TParam> source, MethodMetadata<TData, TParam> from)
        {
            foreach (var method in from.OverLoads)
            {
                var duplicationMethod = source.OverLoads.FirstOrDefault(
                    m =>
                    {
                        if (m.Parameters.Count == method.Parameters.Count)
                        {
                            for (int i = 0; i < m.Parameters.Count; i++)
                            {
                                if (m.Parameters[i].Type != method.Parameters[i].Type)
                                {
                                    return false;
                                }
                            }

                            return true;
                        }

                        return false;
                    });

                if (duplicationMethod == null)
                {
                    source.AddMethod(method);
                }
            }
        }
    }
}
